import re
from pathlib import Path

from buildutils import (logger, remove_file, multi_glob, get_spawn,
                        add_system_include, setup_python_env)

Import('env', 'build', 'install', 'libraryTargets')

def defaultSetup(env, subdir, extensions):
    env.Append(CCFLAGS=env['pch_flags'])
    return multi_glob(env, subdir, *extensions)

def applicationSetup(env, subdir, extensions):
    # Add #define variables unique to application.cpp
    escaped_datadir = '\\"' + env['ct_datadir'].replace('\\', '\\\\') + '\\"'
    env.Append(CPPDEFINES={'CANTERA_DATA': escaped_datadir})
    return defaultSetup(env, subdir, extensions)

def globalSetup(env, subdir, extensions):
    # Add #define variables unique to global.cpp
    env.Append(CPPDEFINES={'GIT_COMMIT': '\\"{0}\\"'.format(env['git_commit'])})
    return defaultSetup(env, subdir, extensions)

def baseSetup(env, subdir, extensions):
    # All files in base except for application.cpp
    return [f for f in defaultSetup(env, subdir, extensions)
            if f.name != 'application.cpp' and f.name != 'global.cpp']

# (subdir, (file extensions), (extra setup(env)))
libs = [('base', ['cpp'], baseSetup),
        ('base', ['^application.cpp'], applicationSetup),
        ('base', ['^global.cpp'], globalSetup),
        ('thermo', ['cpp'], defaultSetup),
        ('tpx', ['cpp'], defaultSetup),
        ('equil', ['cpp','c'], defaultSetup),
        ('numerics', ['cpp'], defaultSetup),
        ('kinetics', ['cpp'], defaultSetup),
        ('transport', ['cpp'], defaultSetup),
        ('oneD', ['cpp'], defaultSetup),
        ('zeroD', ['cpp'], defaultSetup),
        ]

localenv = env.Clone()
localenv.Prepend(CPPPATH=[Dir('#include'), Dir('.')])
add_system_include(localenv, Dir('#include/cantera/ext'), 'prepend')
localenv.Append(CCFLAGS=env['warning_flags'])
indicatorEnv = localenv.Clone()  # Get this before any of the PCH complications

if env['CC'] == 'cl' and env['debug']:
    env['use_pch'] = False # PCH doesn't work with per-file PDB

if env['use_pch']:
    if env['CC'] == 'cl':
        env.Command('#build/src/pch/system.h', '#src/pch/system.h',
                    Copy('$TARGET', '$SOURCE'))
        localenv['PCH'], pchobj = localenv.PCH('pch/system.cpp')
        pch = localenv['PCH']
        libraryTargets.append(pchobj)
        localenv['PCHSTOP'] = 'pch/system.h'
    else:
        localenv['precompiled_header'] = File('pch/system.h')
        pch = localenv.GchSh('#src/pch/system.h.gch',
                             localenv['precompiled_header'])
else:
    remove_file('src/pch/system.h.gch')
    localenv['pch_flags'] = []
    pch = None

for subdir, extensions, setup in libs:
    env2 = localenv.Clone()
    source = setup(env2, subdir, extensions)
    objects = env2.SharedObject(source)
    env2.Depends(objects, env2['config_h_target'])
    if pch:
        env2.Requires(objects, pch)
    libraryTargets.extend(objects)

generated_file_nodes = []

# Handle generated CLib separately, as generated files have different location
auto_path = Path(Dir("#interfaces/sourcegen/src/sourcegen/headers").abspath)
yaml_files = list(auto_path.glob("ct*.yaml"))

env2 = localenv.Clone()
env2.Append(CCFLAGS=env2["pch_flags"])
# Need to update include paths, where second entry is for application.h
env2.Prepend(CPPPATH=["#interfaces/clib/include", "#src/base"])

objects = []
for yaml_file in yaml_files:
    # compile with explicit source and target files
    base_name = yaml_file.stem
    source_file = Dir("#interfaces/clib/src").File(f"{base_name}.cpp")
    target_file = Dir("#build/src/clib").File(f"{base_name}.os")
    obj = env2.SharedObject(
        target_file,
        source_file)

    env2.Depends(obj, env2["config_h_target"])
    if pch:
        env2.Requires(obj, pch)

    generated_file_nodes.append(source_file)
    objects.append(obj)

libraryTargets.extend(objects)

# Handling of extensions written in Python
if env["python_package"] == "y":
    pyenv = setup_python_env(localenv.Clone())
    pyenv['PCH'] = ''  # ignore precompiled header here

    if env["OS"] == "Windows":
        escaped_home = '\\"' + pyenv["py_base"].replace("\\", "\\\\") + '\\"'
        pyenv.Append(CPPDEFINES={"CT_PYTHONHOME": escaped_home})

    pyenv.Append(LIBS=pyenv["py_libs"], LIBPATH=pyenv["py_libpath"])
    shim = pyenv.SharedObject("extensions/pythonShim.cpp")
    pylibname = f"../lib/cantera_python{pyenv['py_version_short'].replace('.', '_')}"
    lib = build(pyenv.SharedLibrary(pylibname, shim, SPAWN=get_spawn(pyenv)))
    install("$inst_shlibdir", lib)


# build the Cantera static library
staticIndicator = indicatorEnv.SharedObject('extensions/canteraStatic.cpp')
lib_target = localenv.StaticLibrary("../lib/cantera", libraryTargets + staticIndicator,
                                    SPAWN=get_spawn(localenv))
localenv.Depends(lib_target, generated_file_nodes)

lib = build(lib_target)
localenv.Depends(lib, localenv['config_h_target'])
install('$inst_libdir', lib)
env['cantera_staticlib'] = lib

localenv.Append(LIBS=localenv['external_libs'],
                LIBPATH=localenv['sundials_libdir'] + localenv['blas_lapack_dir'])

def create_def_file(target, source, env):
    # Adapted from https://stackoverflow.com/a/58958294
    startPoint = False
    # Avoid exporting some unnecessary symbols
    exclusions = [
        lambda name: name.startswith(('??_G', '??_E')), # deleting destructors; generate warning LNK4102
        lambda name: name.startswith(('??_C', '__real@', '__xmm')), # various constants
        lambda name: name.startswith(('??$forward', '??$addressof', '??$construct', '??$destroy')),
        lambda name: name.startswith(('SUN', 'N_V', 'CV', 'cv', 'IDA', 'ida')), # Sundials
        lambda name: '<lambda_' in name, # lambdas
        lambda name: '@boost@' in name,
        lambda name: ('@Eigen@' in name or '@YAML@' in name) and 'Cantera' not in name,
        lambda name: '$vector@' in name and 'Cantera' not in name,
        lambda name: 'char_traits' in name and 'Cantera' not in name and 'fmt' not in name,
        lambda name: '_Tree' in name or '$_Hash' in name, # standard library internals
        lambda name: re.match(r'[\?$]+_[A-Z][a-z_]+@', name), # standard library methods
    ]

    func_count = 0
    include_count = 0
    with open(target[0].abspath, 'w') as outfile, open(source[0].abspath, 'r') as infile:
        outfile.write('EXPORTS\n')

        for l in infile:
            l_str = l.strip()
            if startPoint and l_str == 'Summary': # end point
                break
            if not startPoint and "public symbols" in l_str:
                startPoint = True
                continue
            func_count += 1
            if startPoint and l_str:
                funcName = l_str.split(' ')[-1]
                if not any(test(funcName) for test in exclusions):
                    include_count += 1
                    outfile.write("    " + funcName + "\n")

    logger.info(f"Exported {include_count} out of {func_count} functions in .def file")

# Build the Cantera shared library
if localenv['renamed_shared_libraries']:
    sharedName = '../lib/cantera_shared'
else:
    sharedName = '../lib/cantera'

sharedIndicator = indicatorEnv.SharedObject('extensions/canteraShared.cpp')

if env['CC'] == 'cl':
    # For MSVC, use the static library to create a .def file listing all
    # symbols to export in the shared library.
    dump = localenv.Command('cantera.dump', lib,
                            'dumpbin /LINKERMEMBER:1 $SOURCE /OUT:$TARGET')
    def_file = localenv.Command('cantera_shared.def', dump, create_def_file)
    localenv.Append(LINKFLAGS=['/DEF:build/src/cantera_shared.def'])

if localenv['versioned_shared_library']:
    lib = build(localenv.SharedLibrary(sharedName, libraryTargets + sharedIndicator,
                                        SPAWN=get_spawn(localenv),
                                        SHLIBVERSION=localenv['cantera_pure_version']))
    install(localenv.InstallVersionedLib, '$inst_shlibdir', lib)
else:
    lib = build(localenv.SharedLibrary(sharedName, libraryTargets + sharedIndicator,
                                       SPAWN=get_spawn(localenv)))
    for libfile in lib:
        if libfile.name.endswith(('.exp', '.lib')):
            # On Windows, these are the export/import libraries used when linking, which
            # should be installed in the same location as static libraries
            install('$inst_libdir', libfile)
        else:
            install('$inst_shlibdir', libfile)

if env["OS"] == "Darwin":
    localenv.AddPostAction(lib,
        Action(f"install_name_tool -id @rpath/{lib[0].name} {lib[0].get_abspath()}"))
elif env["CC"] == "cl":
    env.Depends(lib, def_file)

env['cantera_shlib'] = lib
localenv.Depends(lib, localenv['config_h_target'])
